// TODO: CODE REVIEW & CLEANUP!
//
// Driver.cs
//
// Implements the Driver abstract base class and related types.
//

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using HomeUX.Networking;
using HomeUX.Server;
using HomeUX.Synchronization;
using HomeUX.Utilities;
#if SILVERLIGHT
using System.IO.IsolatedStorage;
#endif

namespace HomeUX.DriverModel
{

/// <summary>
/// The base class for a HomeUX driver.
/// </summary>
///
/// <remarks>
/// <para>
/// When the application is finished using a <r>Driver</r> instance, <n>Dispose</n>
/// must be called on it.
/// </para>
/// </remarks>
///
[DebuggerDisplay("Device {Name}")]
public abstract class Driver : IDisposable
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Implementation of the *optional* request queuing mechanism.
    //

    /// <summary>
    /// The currently-executing request.
    /// </summary>
    Request _currentRequest;

    /// <summary>
    /// The queue of requests to execute after the current request is complete.
    /// </summary>
    List<Request> _futureRequests = new List<Request>();

    /// <summary>
    /// The <r>WorkItem</r> used to asynchronously execute the next request in <r>_futureRequests</r>.
    /// </summary>
    WorkItem _executeNextRequestWorkItem;

    ///////////////////////////////////////////////////////////////////////////////////////////////

    DriverState _driverState;

    int _busyCount;

    /*
    // holds the value of IsInitialized
    bool _isInitialized;

    // holds the value of IsEnabled
    bool _isEnabled = true;
    */

    // holds the value of MessagePrefix
    string _messagePrefix;

    // the root SyncNode of the device within PseudoClientTree
    SyncNode _deviceRootNode;

    //List<ExtraAttributeChecker> _extraAttributeCheckers = new List<ExtraAttributeChecker>();

    public int BusyCount
    {
        get
        {
            return _busyCount;
        }
        set
        {
            if (value < 0)
                throw new ArgumentException("BusyCount underflow", "value");
            bool needToUpdate = (_busyCount == 0) != (value == 0);
            _busyCount = value;
            if (needToUpdate)
                BroadcastDeviceStatus(true);
            //SyncNode syncNode = (SyncNode)PseudoClientTree.CreateNode(
            //    SyncPath.Build("DeviceStatus", Name, "BusyCount"));
            //syncNode.Value = value;
        }
    }

    public bool IsDisposed { get; private set; }

    protected DeviceParameters DeviceParameters { get; private set; }

    // this will be non-null during the call to OnInitialize, and the first call to OnEnable --
    // when it's non-null, the driver may use this information, which is its tree from the last
    // time HomeUX was run (or the tree that the user wants the driver to restore to)
    protected ReadOnlySyncNode PersistedTree { get; private set; }

    protected SyncNode PseudoClientTree
    {
        get
        {
            return (SyncNode)SyncServer.PseudoClient.SyncTree;
        }
    }

	internal bool PersistedStateNeedsSaving { get; set; }

    // may throw IOException or MessageProtocolException
    internal void PersistState()
    {
        if (!IsEnabled)
        {
            //DnUtil.Trace(MessagePrefix + "NOT persisting state since device is disabled");
            return;
        }

        //DnUtil.Trace(MessagePrefix + "Persisting state");

        // TODO: ensure device name does not contain any characters illegal in a file name

        string filePath = PersistedFilePath;
#if SILVERLIGHT
        using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForSite())
        {
            store.CreateDirectory(Path.GetDirectoryName(filePath));
            using (IsolatedStorageFileStream stream = store.CreateFile(filePath))
                PersistState(stream);
        }
#else
        string csvFilePath = Path.Combine(Path.GetDirectoryName(filePath),
            Path.GetFileNameWithoutExtension(filePath) + ".csv");
        string tempFilePath = filePath + ".temp";
        Directory.CreateDirectory(Path.GetDirectoryName(tempFilePath));
        using (FileStream stream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write))
            PersistState(stream);
        using (StreamWriter writer = new StreamWriter(csvFilePath))
            WriteSyncNodeAsCsv(_deviceRootNode, writer);
        File.Delete(filePath);
        File.Move(tempFilePath, filePath);
#endif
    }

#if !SILVERLIGHT
    void WriteSyncNodeAsCsv(SyncNodeBase syncNode, TextWriter writer)
    {
        if (syncNode.Value != null)
        {
            string stringValue;
            if ((stringValue = syncNode.Value as string) != null)
			{
                writer.WriteLine("{0},{1}", CsvEncode(syncNode.Path.ToString()),
					CsvEncode(stringValue));
			}
            else
			{
                writer.WriteLine("{0},{1}", CsvEncode(syncNode.Path.ToString()),
					CsvEncode(syncNode.Value));
			}
        }
        else
            writer.WriteLine("{0},null", CsvEncode(syncNode.Path.ToString()));

        if (syncNode.HasChildren)
        {
            foreach (SyncNodeBase childNode in syncNode.Children)
                WriteSyncNodeAsCsv(childNode, writer);
        }
    }

	static string CsvEncode(object value)
	{
		string stringValue;
        if ((stringValue = value as string) != null)
            return String.Format("\"{0}\"", stringValue.Replace("\"", "\"\""));
        else
            return String.Format(CultureInfo.InvariantCulture, "{0}", value);
	}
#endif

	string PersistedFilePath
	{
		get
		{
			string filePath = Path.Combine("DeviceState", String.Format("{0}.xml", Name));
#if !SILVERLIGHT
			filePath = Path.Combine(SyncServer.StorageDirectoryPath, filePath);
#endif
			return filePath;
		}
	}

    // may throw IOException or MessageProtocolException
    void PersistState(FileStream stream)
    {
        var settings = new XmlWriterSettings();
        settings.OmitXmlDeclaration = true;
        settings.Indent = true;
        using (XmlWriter writer = XmlWriter.Create(stream, settings))
            PersistSyncNode(_deviceRootNode, writer);
    }

    // may throw MessageProtocolException
    void PersistSyncNode(SyncNodeBase syncNode, XmlWriter writer)
    {
        writer.WriteStartElement("SyncNode");
        writer.WriteAttributeString("Name", syncNode.Name);
        SyncNodeUpdate.SerializeValue(syncNode.Value, writer);
        if (syncNode.HasChildren)
        {
            foreach (SyncNodeBase childNode in syncNode.Children)
                PersistSyncNode(childNode, writer);
        }
        writer.WriteEndElement();
    }

    static ReadOnlySyncNode DepersistSyncNode(Stream stream)
    {
        using (var reader = XmlReader.Create(stream))
        {
            reader.MoveToContent();
            return DepersistSyncNode(reader);
        }
    }

    static ReadOnlySyncNode DepersistSyncNode(XmlReader reader)
    {
        string name = reader.GetAttribute("Name");
        if (name == null)
            throw new MessageProtocolException(null, "Missing Name attribute");
        object value = SyncNodeUpdate.DeserializeValue(reader);
        ReadOnlySyncNode syncNode = new ReadOnlySyncNode(name, value);

        if (!reader.IsEmptyElement)
        {
            reader.Read();
            while (true)
            {
                XmlNodeType nodeType = reader.MoveToContent();
                if (nodeType == XmlNodeType.EndElement)
                    break;
                else
                if (nodeType == XmlNodeType.Element)
                {
                    if (reader.Name == "SyncNode")
                    {
                        ReadOnlySyncNode childNode = DepersistSyncNode(reader);
                        syncNode.AddChildNode(childNode);
                    }
                    else
                        reader.Skip();
                }
                reader.Read();
            }
        }

        return syncNode;
    }

    internal void Initialize(DeviceParameters param)
    {
        DeviceParameters = param;

        _messagePrefix = String.Format("\"{0}\" device: ", DeviceParameters.Name).Replace("{", "(").Replace("}", ")");
    
        // TODO: disallow backslash in Name!
        _deviceRootNode = (SyncNode)this.PseudoClientTree.CreateNode(SyncPath.FromString(Name));
        _deviceRootNode.DescendantChanged += _deviceRootNode_DescendantChanged;

        try
        {
            string filePath = PersistedFilePath;
#if SILVERLIGHT
			using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForSite())
			{
                if (store.FileExists(filePath))
                {
                    using (IsolatedStorageFileStream stream = store.OpenFile(filePath, FileMode.Open))
                        PersistedTree = DepersistSyncNode(stream);
                }
			}
#else
            if (File.Exists(filePath))
            {
                using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                    PersistedTree = DepersistSyncNode(stream);
            }
#endif
        }
        catch (Exception ex)
        {
            if ((ex is IOException) || (ex is XmlException) || (ex is MessageProtocolException)
#if SILVERLIGHT
                || (ex is IsolatedStorageException)
#endif
               )
            {
                LogWarning("Failed to depersist state: {0}", Util.FormatExceptionMessage(ex));
            }
            else
                throw;
        }

        if (PersistedTree == null)
            PersistedTree = new ReadOnlySyncNode(Name, null);

        // create a parent node one level above <PersistedTree>
        ReadOnlySyncNode rootNode = new ReadOnlySyncNode(null, null);
        rootNode.AddChildNode(PersistedTree);

        try
        {
            OnInitialize();

            if (Configuration != null)
                Configuration.CheckForExtraElementsAndAttributes();

            //foreach (ExtraAttributeChecker extras in _extraAttributeCheckers)
            //    extras.CheckForExtraAttributes();
            _driverState = DriverState.Disabled;

            bool shouldEnable;
            if ((this is ISingleInstanceBuiltInDriver) || (SyncServer.InitialDeviceStatus == null))
                shouldEnable = true;
            else
            if (!SyncServer.InitialDeviceStatus.HasChildren)
            {
                // this situation usually arises from killing HomeUX in the debugger before it
                // has a chance to write DeviceStatus.xml -- in this case, enable all devices
                shouldEnable = true;
            }
            else
            {
                SyncNodeBase syncNode = SyncServer.InitialDeviceStatus.TryFindNode(
                    SyncPath.FromString(Name));
                shouldEnable = (syncNode != null) && Object.Equals(syncNode.Value, true);
            }
            if (shouldEnable)
                Enable();
        }
#if !NOCATCH
        catch (Exception)
        {
            _driverState = DriverState.FailedInitialization;
            throw;
        } // exception here? try defining NOCATCH
#else
        finally
        {
        }
#endif
    }

    void _deviceRootNode_DescendantChanged(object sender, DescendantChangedEventArgs e)
    {
        //DnUtil.Trace("** {0} CHANGED **", ((SyncNode)sender).Path);
        PersistedStateNeedsSaving = true;
    }

    /// <summary>
    /// Tells the driver to start the process of initializing -- this process may complete
    /// asynchronously.  When initialization is complete,
    /// the driver needs to set TODO-IsInitialized-??? to <n>true</n>.  If initialization fails,
    /// the driver needs to set <r>IsEnabled</r> to <n>false</n>.
    /// </summary>
    protected virtual void OnInitialize()
    {
    }

    protected void LogInfo(string format, params object[] args)
    {
        if (SyncServer.LogDestination != null)
            SyncServer.LogDestination.LogInfo(MessagePrefix + format, args);
    }

    protected void LogWarning(string format, params object[] args)
    {
        if (SyncServer.LogDestination != null)
            SyncServer.LogDestination.LogWarning(MessagePrefix + format, args);
    }

    // if this returns false, driver cannot be enabled because it failed initialization; in this
    // state, calling Enable() will cause an exception to be thrown
    public bool CanEnable
    {
        get
        {
            return _driverState != DriverState.FailedInitialization;
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    public bool IsEnabled
    {
        get
        {
            return _driverState == DriverState.Enabled;
        }
    }

    // asynchronously begins the process of enabling the driver
    public void Enable()
    {
        // do nothing if the driver is already enabled
        if (_driverState == DriverState.Enabled)
        {
            BroadcastDeviceStatus(false); // e.g. remove hourglass
            return;
        }

        // do nothing if the driver is already set to be enabled after its current
        // operation is complete
        if (_driverState == DriverState.DisablingThenEnable)
            return;

        // make sure it's possible to enable the driver
        if (!CanEnable)
        {
            BroadcastDeviceStatus(false); // e.g. remove hourglass
            return;
        }

        // if the caller wants to enable the driver while it is in the process of disabling,
        // mark the driver for enabling once disabling completes
        if (_driverState == DriverState.Disabling)
        {
            _driverState = DriverState.DisablingThenEnable;
            return;
        }

        // if the driver is already in the process of being enabled, do nothing further
        if (_driverState == DriverState.Enabling)
            return;

        _driverState = DriverState.Enabling;

        // tell the derived object to begin enabling
        try
        {
            OnEnable();
        }
#if !NOCATCH
        catch (Exception ex)
        {
            if ((ex is DriverException) || (ex is VirtualSerialPortException))
            {
                _driverState = DriverState.Disabled;
                return;
            }
            else
                throw;
        }
#endif
        finally
        {
        }
    }

    /// <summary>
    /// TODO: tells the derived object to begin enabling
    /// contract:
    ///    -- app or other drivers call Enable() to enable a driver;
    ///    -- service calls OnEnable on driver;
    ///    -- driver calls EnableCompleted synchronously or asynchronously to indicate success or failure
    /// </summary>
    protected virtual void OnEnable()
    {
        // default behavior is to immediately report the enabling process as being complete and
        // successful
        EnableCompleted(true);
    }

    protected void EnableCompleted(bool success)
    {
        if (success)
        {
            // don't let the driver use <PersistedTree> if the driver is subsequently disabled and re-enabled
            PersistedTree = null;
        }

        // if Disable() was called while the driver was enabling, we need to begin the process of
        // disabling now (unless enabling failed, in which case we're already disabled)
        if (_driverState == DriverState.EnablingThenDisable)
        {
            if (success)
            {
                _driverState = DriverState.Enabled;
                Disable();
            }
            else
                _driverState = DriverState.Disabled;
            return;
        }

        // the derived object can only call this method if <_driverState> is either Enabling
        // (checked here) or EnablingThenDisable (handled above)
        if (_driverState != DriverState.Enabling)
        {
            throw new InvalidOperationException(String.Format(
                "EnableCompleted was called when DriverState was {0}", _driverState));
        }

        _driverState = (success ? DriverState.Enabled : DriverState.Disabled);

        if (success && !(this is ISingleInstanceBuiltInDriver))
        {
            LogInfo("Device is enabled");

            // check if all devices are enabled now
            if (SyncServer.AreAllDevicesEnabled())
            {
                if (SyncServer.LogDestination != null)
                    SyncServer.LogDestination.LogInfo("All devices are enabled");
            }
        }

        // inform any clients that happen to be connected currently
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            // on success, report status change to everyone; on failure, report only to clients in
            // StatusChangeOriginators (so they can remove their hourglasses), since for other
            // clients the state won't have changed
            if (success || StatusChangeOriginators.Keys.Contains(sessionId))
            {
                SerializeStateForClient(sessionId, messageBuilder.Writer);
                SyncServer.DeviceStatusDriver.SerializeDeviceStatus(this, sessionId, messageBuilder.Writer, false);
            }
        });

        SyncServer.SendAllOutgoingMessageElements();
        PersistState();
    }

    // asynchronously begins the process of disabling the driver
    public void Disable()
    {
        // do nothing if the driver is already disabled
        if (_driverState == DriverState.Disabled)
        {
            BroadcastDeviceStatus(false); // e.g. remove hourglass
            return;
        }

        // do nothing if the driver is already set to be disabled after its current
        // operation is complete
        if (_driverState == DriverState.EnablingThenDisable)
            return;

        // do nothing if the driver failed initialization
        if (!CanEnable)
        {
            BroadcastDeviceStatus(false); // e.g. remove hourglass
            return;
        }

        // if the caller wants to disable the driver while it is in the process of enabling,
        // mark the driver for disabling once enabling completes
        if (_driverState == DriverState.Enabling)
        {
            _driverState = DriverState.EnablingThenDisable;
            return;
        }

        // if the driver is already in the process of being disabled, do nothing further
        if (_driverState == DriverState.Disabling)
            return;

        _driverState = DriverState.Disabling;

        // cancel the current request, if any
        if (_currentRequest != null)
        {
            _currentRequest.Cancel();
            _currentRequest = null;
        }

        // cancel queued requests, if any
        while (_futureRequests.Count > 0)
        {
            _currentRequest = _futureRequests[0];
            _currentRequest.OverrideOnExit = delegate { };
            _futureRequests.RemoveAt(0);
            try
            {
                _currentRequest.Cancel();
            }
            finally
            {
                _currentRequest = null;
            }
        }

        // tell the derived object to begin disabling
        try
        {
            OnDisable();
        }
        catch (Exception ex)
        {
            if ((ex is DriverException) || (ex is VirtualSerialPortException))
                _driverState = DriverState.Disabled;
            else
                throw;
        }
    }


    /// <summary>
    /// TODO: tells the derived object to begin disabling
    /// contract:
    ///    -- app or other drivers call Disable() to disable a driver;
    ///    -- service calls OnDisable on driver;
    ///    -- driver calls DisableCompleted synchronously or asynchronously to indicate success or failure
    /// </summary>
    protected virtual void OnDisable()
    {
        // default behavior is to immediately report the disabling process as being complete
        DisableCompleted();
    }

    // information about clients who have requested a change to the enabled/disabled state of the
    // driver and have not yet been informed of a subsequent change in driver state; key is
    // session ID
    internal Dictionary<Guid, StatusChangeOriginator> StatusChangeOriginators =
        new Dictionary<Guid, StatusChangeOriginator>();

    internal class StatusChangeOriginator
    {
        /// <summary>
        /// Identifies the client that sent the driver enable/disable request(s).
        /// </summary>
        public Guid SessionId;

        /// <summary>
        /// The number of requests from this client to enable or disable this driver.
        /// </summary>
        public int RequestCount;

        /// <summary>
        /// <n>true</n> if the request(s) are to enable the driver, <n>false</n> if they are to
        /// disable the driver.  Note that if a mix of requests are received, they are combined
        /// into the last-received request; for example, a request to enable followed immediately
        /// by a request to disable is treated the same as two requests to disable.
        /// </summary>
        public bool RequestsAreToEnable;
    }

    protected void DisableCompleted()
    {
        // if Enable() was called while the driver was disabling, we need to begin the process of
        // enabling now
        if (_driverState == DriverState.DisablingThenEnable)
        {
            _driverState = DriverState.Disabled;
            Enable();
            return;
        }

        // the derived object can only call this method if <_driverState> is either Disabling
        // (checked here) or DisablingThenEnable (handled above)
        if (_driverState != DriverState.Disabling)
        {
            throw new InvalidOperationException(String.Format(
                "DisableCompleted was called when DriverState was {0}", _driverState));
        }

        _driverState = DriverState.Disabled;

        LogInfo("Device is disabled");

        // inform any clients that happen to be connected currently
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            SerializeStateForClient(sessionId, messageBuilder.Writer);
            SyncServer.DeviceStatusDriver.SerializeDeviceStatus(this, sessionId, messageBuilder.Writer, false);
        });
    }

    void BroadcastDeviceStatus(bool onlyBusyCount)
    {
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            SyncServer.DeviceStatusDriver.SerializeDeviceStatus(this, sessionId, messageBuilder.Writer, onlyBusyCount);
        });
    }

    public string Name
    {
        get
        {
            return DeviceParameters.Name;
        }
    }

    public WorkQueue WorkQueue
    {
        get
        {
            return DeviceParameters.WorkQueue;
        }
    }

    public ConfigurationElement Configuration
    {
        get
        {
            return DeviceParameters.Configuration;
        }
    }

    protected SyncServer SyncServer
    {
        get
        {
            return DeviceParameters.SyncServer;
        }
    }

    // returns a string of the form "<device-description>: " that can be used in messages displayed to the user;
    // may be used within a format string -- it's guaranteed not to contain "{" or "}" characters
    public string MessagePrefix
    {
        get
        {
            return _messagePrefix;
        }
    }

    /// <summary>
    /// Writes a formatted string to the debug command window.
    /// </summary>
    /// 
    /// <param name="format">A format string.</param>
    /// 
    /// <param name="args">Formatting arguments.</param>
    ///
    protected internal void Trace(string format, params object[] args)
    {
        Trace(false, format, args);
    }

    /// <summary>
    /// Writes a formatted string to the debug command window.  Optionally highlights the text.
    /// </summary>
    /// 
    /// <param name="highlight"><n>true</n> to highlight the text.</param>
    /// 
    /// <param name="format">A format string.</param>
    /// 
    /// <param name="args">Formatting arguments.</param>
    ///
    protected internal void Trace(bool highlight, string format, params object[] args)
    {
        Util.Trace(highlight, MessagePrefix + format, args);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Protected Methods (for driver use)
    //

    protected object NullIfDisabled(object value)
    {
        if (IsEnabled)
            return value;
        else
            return null;
    }

    protected void SendMessageElements(Guid sessionId, Action<MessageBuilder> del)
    {
        SyncServer.SendMessageElements(sessionId, del);
    }

    protected void SendMessageElements(IList<Guid> sessionIds, Action<Guid, MessageBuilder> del)
    {
        SyncServer.SendMessageElements(sessionIds, del);
    }

    protected void BroadcastMessageElements(Action<Guid, MessageBuilder> del)
    {
        SyncServer.BroadcastMessageElements(del);
    }

    /// <summary>
    /// Sends a <r>SyncNodeUpdate</r> to all connected clients.  If the <r>SyncNodeUpdate</r>
    /// <s>SyncNodeUpdate.IsFailure</s> equal to <n>true</n>, then the update is only sent to
    /// clients that originated the update request.
    /// </summary>
    /// 
    /// <param name="operation">The <r>SyncNodeOperation</r> to broadcast.  NOTE: <pr>operation</pr>
    ///     is modified by this method.</param>
    /// 
    /// <param name="originators">The session IDs of clients for whom this update is the
    ///     response to an update sent from that client.  If a given client sent N
    ///     <r>SyncNodeUpdate</r> messages that are encompassed by this reply, then
    ///     <pr>originators</pr> will include N copies of that client's session ID.</param>
    ///
    protected void BroadcastSyncNodeOperation(SyncNodeOperation operation, IList<Guid> originators)
    {
        var update = operation as SyncNodeUpdate;
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            operation.OriginatedHere = originators.Count(id => id == sessionId);
            if (((update != null) && !update.IsFailure) || (operation.OriginatedHere > 0))
                operation.Serialize(messageBuilder.Writer, false);
        });
    }

    protected void BroadcastSyncNodeOperation(SyncNodeOperation operation, Guid originator)
    {
        BroadcastSyncNodeOperation(operation, new List<Guid> { originator });
    }

    protected void BroadcastSyncNodeUpdate(SyncPath path, object value, IList<Guid> originators)
    {
        BroadcastSyncNodeOperation(new SyncNodeUpdate()
        {
            Path = path,
            Value = value
        }, originators);
    }

    static readonly IList<Guid> _noOriginators = new List<Guid>();

    public static IList<Guid> NoOriginators
    {
        get
        {
            return _noOriginators;
        }
    }

    protected void BroadcastSyncNode(SyncNode syncNode, IList<Guid> originators)
    {
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            var update = new SyncNodeUpdate()
            {
                Path = syncNode.Path,
                Value = syncNode.Value,
                OriginatedHere = originators.Count(id => id == sessionId)
            };
            if (!update.IsFailure || (update.OriginatedHere > 0))
                update.Serialize(messageBuilder.Writer, false);
        });
    }

    protected void BroadcastSyncNode(SyncNode syncNode, Guid originator)
    {
        BroadcastSyncNode(syncNode, new List<Guid> { originator });
    }

	protected void ReportAlert(MessageAlertType alertType, string format, params object[] args)
	{
        if (alertType == MessageAlertType.Information)
            LogInfo(format, args);
        else
            LogWarning(format, args);

		BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
		{
			messageBuilder.WriteAlertElement(alertType, MessagePrefix + format, args);
		});
	}

#if false
    protected ExtraAttributeChecker GetExtraAttributeChecker(ConfigurationElement element)
    {
        var extras = new ExtraAttributeChecker(element);
        _extraAttributeCheckers.Add(extras);
        if (object.ReferenceEquals(element, Configuration))
        {
            extras.MarkAttributeAsValid("Name");
            extras.MarkAttributeAsValid("Driver");
        }
        return extras;
    }
#endif

    protected void SerializeSyncNodeUpdate(XmlWriter writer, object value, params object[] pathParts)
    {
        SyncNodeUpdate update = new SyncNodeUpdate()
        {
            Path = SyncPath.Build(pathParts),
            Value = NullIfDisabled(value)
        };
        update.Serialize(writer, false);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Virtual (Overridable) Methods
    //

    /// <summary>
    /// TODO
    /// </summary>
    /// <param name="sessionId">TODO</param>
    /// <param name="writer">TODO</param>
    public virtual void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        // base class does nothing
    }

    /// <summary>
    /// Called when a client requests an update to a SyncTree node.
    /// </summary>
    ///
    /// <param name="originator">The session ID of the client.</param>
    ///
    /// <param name="update">The requested update.</param>
    ///
    /// <param name="replyBuilder">Where to write a reply to the client, if any.</param>
    ///
    public virtual void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        throw new SyncException("Driver \"{0}\" does not support SyncNode updates", GetType());
    }

    /// <summary>
    /// TODO
    /// </summary>
    /// <param name="originator">TODO</param>
    /// <param name="command">TODO</param>
    /// <param name="replyBuilder">TODO</param>
    public virtual void ProcessSyncNodeCommand(Guid originator, SyncNodeCommand command,
        MessageBuilder replyBuilder)
    {
        throw new SyncException("Driver \"{0}\" does not support SyncNode commands", GetType());
    }

    /// <summary>
    /// Disposes of resources used by this object. 
    /// </summary>
	///
	/// <remarks>
	/// The <r>Driver</r> base class does nothing.
	/// </remarks>
    ///
    public virtual void Dispose()
    {
        IsDisposed = true;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Protected Methods
    //

    protected void BroadcastMessage(Message message)
    {
        SyncServer.BroadcastMessage(message);
    }

    protected string GetRequiredPathPart(string[] pathParts, int index)
    {
        if (index >= pathParts.Length)
        {
            throw new SyncException("Invalid SyncNode path (too short): {0}",
                String.Join("\\", pathParts));
        }
        return pathParts[index];
    }

    protected void NoMorePathParts(string[] pathParts, int length)
    {
        if (pathParts.Length > length)
        {
            throw new SyncException("Invalid SyncNode path (too long): {0}",
                String.Join("\\", pathParts));
        }
    }

#if false

    /// <summary>
    /// Returns an <n>AsyncCallback</n> for use in an asynchronous call to a method which
    /// has no return value.
    /// </summary>
    /// <param name="endMethod"></param>
    /// <param name="onDone"></param>
    /// <returns></returns>
    /// <remarks>
    /// 
    /// 
    /// Example, where someProxy is WCF service proxy (generated by svcutil.exe) which
    /// has a method "void Foo(int)":
    /// 
    /// someProxy.BeginFoo(123, GetAsyncCallback(someProxy, OnDoneFoo), null);
    /// 
    /// You need to implement OnDoneFoo(), which is called with the result.
    /// OnDoneFoo() will be called on the <r>WorkQueue</r> thread.
    /// </remarks>
    ///
    protected AsyncCallback GetAsyncCallback(Action<IAsyncResult> endMethod, Action onDone)
    {
        return new AsyncCallback(delegate(IAsyncResult result)
        {
            // note: this is on a different thread!
            endMethod(result);
            WorkQueue.Add(delegate
            {
                onDone();
            });
        });
    }

    /// <summary>
    /// Returns an <n>AsyncCallback</n> for use in an asynchronous call to a method which
    /// has a return value.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="endMethod"></param>
    /// <param name="onDone"></param>
    /// <returns></returns>
    /// <remarks>
    /// Example, where someProxy is WCF service proxy (generated by svcutil.exe) which
    /// has a method "string Foo(int)":
    /// 
    /// someProxy.BeginFoo(123, GetAsyncCallback(someProxy, OnDoneFoo), null);
    /// 
    /// You need to implement OnDoneFoo(string), which is called with the result.
    /// OnDoneFoo() will be called on the <r>WorkQueue</r> thread.
    /// </remarks>
    protected AsyncCallback GetAsyncCallback<T>(Func<IAsyncResult, T> endMethod, Action<T> onDone)
    {
        return new AsyncCallback(delegate(IAsyncResult result)
        {
            // note: this is on a different thread!
            T returnValue;
            try
            {
                returnValue = endMethod(result);
            }
			catch (Exception ex)
			{
				// note that FaultException derives from CommunicationException
                if ((ex is TimeoutException)
#if !SILVERLIGHT
                    || (ex is EndpointNotFoundException) || (ex is CommunicationException)
#endif
					)
				{
					returnValue = default(T);
				}
				else
					throw;
			}

            WorkQueue.Add(delegate
            {
                onDone(returnValue);
            });
        });
    }

#endif

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Implementation of *optional* request mechanism
    //

    protected void QueueRequest(Request request, bool waitForMoreRequests)
    {
        if ((_currentRequest == null) && (_futureRequests.Count == 0) && !waitForMoreRequests)
        {
            _currentRequest = request;
            BusyCount++;
            _currentRequest.Start();
            return;
        }

        _futureRequests.Add(request);

        if ((_currentRequest == null) && (_executeNextRequestWorkItem == null))
            _executeNextRequestWorkItem = WorkQueue.Add(ExecuteNextRequest);
    }

    protected void DoneCurrentRequest(Request request)
    {
        if ((_currentRequest == null) || !object.ReferenceEquals(_currentRequest, request))
            throw new InvalidOperationException("Completed request isn't the current request");
        BusyCount--;
        _currentRequest = null;
        ExecuteNextRequest();
    }

    void ExecuteNextRequest()
    {
        _executeNextRequestWorkItem = null;

        // do nothing if Dispose() was called, or if the driver is disabled
        if (IsDisposed || !IsEnabled)
            return;

        if (_currentRequest != null)
            throw new InvalidOperationException("Current request isn't done yet");

        MergeRequests(_futureRequests);

        if (_futureRequests.Count > 0)
        {
            _currentRequest = _futureRequests[0];
            _futureRequests.RemoveAt(0);
            BusyCount++;
            _currentRequest.Start();
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="futureRequests">TODO</param>
    ///
    protected virtual void MergeRequests(IList<Request> futureRequests)
    {
    }

    // nested classes

    /// <summary>
    /// TODO
    /// </summary>
    protected abstract class Request : StateMachine
    {
        public Driver Driver { get; set; }

        /// <summary>
        /// The <r>SyncNodeOperation</r> that was the cause of this request, or <n>null</n> if this
        /// request doesn't have an associated <r>SyncNodeOperation</r>.  If non-<n>null</n>,
        /// then <r>OnExit</r> will broadcast <r>Operation</r> back to clients (if <r>Operation</r>
        /// is actually a <r>SyncNodeUpdate</r> then <r>SyncNodeUpdate.Value</r> will be
        /// <r>Message.FailedValue</r> if <r>Error</r> is non-<n>null</n>).
        /// </summary>
        public SyncNodeOperation Operation { get; set; }

        /// <summary>
        /// If the operation fails, this will be an error message, otherwise it will be
        /// <n>null</n>.  See <r>Operation</r> for more information.
        /// </summary>
        public string Error { get; set; }

        /// <summary>
        /// Contains the session ID of the client that initiated the request.  If multiple
        /// requests have been merged into one, this contains the session IDs from all those
        /// requests.
        /// </summary>
        public List<Guid> Originators { get; set; }

        public Guid SingleOriginator
        {
            get
            {
                if (Originators.Count != 1)
                {
                    throw new InvalidOperationException(
                        "Internal error: expected a single originator");
                }
                return Originators[0];
            }
        }

        public Action OverrideOnExit { get; set; }

        public void Cancel()
        {
            Error = "Request was cancelled";
            OnExit();
        }

        /// <summary>
        /// TODO
        /// </summary>
        protected override void OnExit()
        {
            if (Error != null)
            {
                // the request failed
                Driver.ReportAlert(MessageAlertType.Warning, "{0} request failed: {1}",
                    GetType().Name, Error);
                var update = Operation as SyncNodeUpdate;
                if (update != null)
                    update.Value = Message.FailedValue;
            }

            if (Operation != null)
                Driver.BroadcastSyncNodeOperation(Operation, Originators);

            if (this is IEnableRequest)
            {
                if (Error != null)
                {
                    Driver.ReportAlert(MessageAlertType.Warning,
                        "Device disabled because enabling failed");
                    Driver.EnableCompleted(false);
                }
                else
                    Driver.EnableCompleted(true);
            }
            else
            if (this is IDisableRequest)
                Driver.DisableCompleted();

            if (OverrideOnExit != null)
                OverrideOnExit();
            else
                Driver.DoneCurrentRequest(this);
        }
    }

    /// <summary>
    /// TODO - drivers using the (optional) request mechanism should tag their "enable driver" request class by
    /// having it implement this interface; doing so will cause the base class to put the driver into
    /// the correct state when the "enable driver" request is complete
    /// </summary>
    public interface IEnableRequest
    {
    }

    /// <summary>
    /// TODO - drivers using the (optional) request mechanism should tag their "disable driver" request class by
    /// having it implement this interface; doing so will cause the base class to put the driver into
    /// the correct state when the "disable driver" request is complete
    /// </summary>
    public interface IDisableRequest
    {
    }
}

public class DeviceParameters
{
    // construct using SyncServer.CreateDeviceParameters
    internal DeviceParameters()
    {
    }

    // TODO: add WorkQueue
    internal SyncServer SyncServer { get; set; }
    public string Name { get; internal set; }
    public ConfigurationElement Configuration { get; internal set; }
    public WorkQueue WorkQueue
    {
        get
        {
            return SyncServer.WorkQueue;
        }
    }
}

public class ConfigurationAttributeValue
{
    public string Value { get; internal set; }
    internal bool WasRead;
}

/// <summary>
/// TODO
/// </summary>
[DebuggerDisplay("ConfigurationElement {Name}")]
public class ConfigurationElement
{
    static ReadOnlyCollection<ConfigurationElement> _noChildren =
        new ReadOnlyCollection<ConfigurationElement>(new List<ConfigurationElement>());

    public string Name { get; private set; }

    public int LineNumber { get; private set; }

    IDictionary<string, ConfigurationAttributeValue> _attributes;

    public string Text { get; private set; }

    IList<ConfigurationElement> _children;

    // true if any of the child attributes of this node were accessed
    public bool WasRead;

    public ICollection<string> AttributeNames
    {
        get
        {
            if (_attributes != null)
                return new ReadOnlyCollection<string>(new List<string>(_attributes.Keys));
            else
                return _emptyStringList;
        }
    }

    static ReadOnlyCollection<string> _emptyStringList =
        new ReadOnlyCollection<string>(new List<string>());

    public string GetRequiredAttribute(string name)
    {
        //if (extras != null)
        //    extras.MarkAttributeAsValid(name);
        ConfigurationAttributeValue value;
        if ((_attributes == null) || !_attributes.TryGetValue(name, out value))
            throw new ConfigurationException(this, "Missing \"{0}\" attribute", name);
        WasRead = true;
        value.WasRead = true;
        return value.Value;
    }

    public string GetOptionalAttribute(string name, string defaultValue)
    {
        //if (extras != null)
        //    extras.MarkAttributeAsValid(name);
        ConfigurationAttributeValue value;
        if ((_attributes == null) || !_attributes.TryGetValue(name, out value))
            return defaultValue;
        WasRead = true;
        value.WasRead = true;
        return value.Value;
    }

    public int GetRequiredIntAttribute(string name)
    {
        string stringValue = GetRequiredAttribute(name);
        try
        {
            return XmlConvert.ToInt32(stringValue);
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
            {
                throw new ConfigurationException(this,
                    "Attribute \"{0}\" doesn't contain a valid integer value: {1}",
                    name, stringValue);
            }
            else
                throw;
        }
    }

    public int GetRequiredIntAttribute(string name, int minValue, int maxValue)
    {
        int intValue = GetRequiredIntAttribute(name);
        if ((intValue < minValue) || (intValue > maxValue))
        {
            throw new ConfigurationException(this,
                "Attribute \"{0}\" value {1} isn't in the range {2} to {3}, inclusive",
                name, intValue, minValue, maxValue);
        }
        return intValue;
    }

    public double GetRequiredDoubleAttribute(string name)
    {
        string stringValue = GetRequiredAttribute(name);
        try
        {
            return XmlConvert.ToDouble(stringValue);
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
            {
                throw new ConfigurationException(this,
                    "Attribute \"{0}\" doesn't contain a valid numeric value: {1}",
                    name, stringValue);
            }
            else
                throw;
        }
    }

    public bool GetRequiredBooleanAttribute(string name)
    {
        string stringValue = GetRequiredAttribute(name);
        try
        {
            return XmlConvert.ToBoolean(stringValue);
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
            {
                throw new ConfigurationException(this,
                    "Attribute \"{0}\" doesn't contain a valid Boolean value: {1}",
                    name, stringValue);
            }
            else
                throw;
        }
    }

    public IList<ConfigurationElement> Children
    {
        get
        {
            if (_children == null)
                return _noChildren;
            else
                return new ReadOnlyCollection<ConfigurationElement>(_children);
        }
    }

    internal ConfigurationElement(string name, int lineNumber,
        IDictionary<string, ConfigurationAttributeValue> attributes,
        string text, IList<ConfigurationElement> children)
    {
        Name = name;
        LineNumber = lineNumber;
        _attributes = attributes;
        Text = text;
        _children = children;
    }

    public string Xml
    {
        get
        {
            var buffer = new StringBuilder(1000);
            var settings = new XmlWriterSettings();
            settings.OmitXmlDeclaration = true;
            settings.Indent = true;
            using (XmlWriter writer = XmlWriter.Create(buffer, settings))
                Serialize(writer);
            return buffer.ToString();
        }
    }

    public void Serialize(XmlWriter writer)
    {
        writer.WriteStartElement(Name);
        if (_attributes != null)
        {
            foreach (KeyValuePair<string, ConfigurationAttributeValue> kvp in _attributes)
                writer.WriteAttributeString(kvp.Key, kvp.Value.Value);
        }
        if (Text != null)
            writer.WriteString(Text);
        if (_children != null)
        {
            foreach (ConfigurationElement child in _children)
                child.Serialize(writer);
        }
        writer.WriteEndElement();
    }

    /// <summary>
    /// Creates an instance of this exception class.  Formats an exception message.
    /// </summary>
    ///
    /// <param name="format">The format string for the exception message.</param>
	///
    /// <param name="args">Formatting arguments.  If no arguments are provided, <pr>format</pr> is
	/// 	used as-is, without formattiong.</param>
    ///
	public ConfigurationException NewConfigurationException(string format, params object[] args)
	{
        format = String.Format("Line {0}: ", LineNumber) + format;
        return new ConfigurationException(this, format, args);
	}

    internal void CheckForExtraElementsAndAttributes()
    {
        if (!WasRead)
            throw new ConfigurationException(this, "Unknown element <{0}>", Name);

        if (_attributes != null)
        {
            foreach (KeyValuePair<string, ConfigurationAttributeValue> kvp in _attributes)
            {
                if (!kvp.Value.WasRead)
                {
                    throw new ConfigurationException(this, "Element <{0}> has an unknown attribute: {1}",
                        Name, kvp.Key);
                }
            }
        }

        if (_children != null)
        {
            foreach (ConfigurationElement child in _children)
                child.CheckForExtraElementsAndAttributes();
        }
    }
}

#if false
// "using (var extras = new ExtraAttributeChecker(element)) { ... }" was abandoned
// because if "..." throws an exception before all MarkAttributeAsValid() calls in "..." have been
// done, extras.Dispose() could cause a ConfigurationException to be thrown which is (a) wrong, and
// (b) masks the original exception
[DebuggerDisplay("ExtraAttributeChecker: {_extras.Count} attributes not accounted for yet")]
public class ExtraAttributeChecker
{
    ConfigurationElement _element;
    StringSet _extras;

    public String[] Extras
    {
        get
        {
            return _extras.ToArray();
        }
    }

    // in general, use Driver.GetExtraAttributeChecker instead
    public ExtraAttributeChecker(ConfigurationElement element)
    {
        _element = element;
        _extras = new StringSet(element.AttributeNames);
    }

    public void MarkAttributeAsValid(string attributeName)
    {
        _extras.Remove(attributeName);
    }

    public void Clear()
    {
        _extras.Clear();
    }

    public void CheckForExtraAttributes()
    {
        if (_extras.Count > 0)
        {
            throw new ConfigurationException(_element, "Unknown attribute(s): {0}",
                String.Join(", ", Extras));
        }
    }
}
#endif

/// <summary>
/// Indicates an error that occurred related to a <r>ConfigurationElement</r>.
/// </summary>
///
public class ConfigurationException : Exception
{
    /// <summary>
    /// Creates an instance of this exception class.  Formats an exception message.
    /// </summary>
    ///
    /// <param name="element">The <r>ConfigurationElement</r> that's related to this exception.</param>
    ///
    /// <param name="format">The format string for the exception message.</param>
	///
    /// <param name="args">Formatting arguments.  If no arguments are provided, <pr>format</pr> is
	/// 	used as-is, without formattiong.</param>
    ///
	public ConfigurationException(ConfigurationElement element, string format, params object[] args) :
		base(String.Format("Line {0}: {1}", element.LineNumber, Util.Format(format, args)))
	{
	}

    /// <summary>
    /// Creates an instance of this exception class.  Formats an exception message.
    /// </summary>
    ///
    /// <param name="element">The <r>ConfigurationElement</r> that's related to this exception.</param>
    ///
    /// <param name="lineOffset">A value to add to the line number of <pr>element</pr> to compute the reported line number.</param>
    ///
    /// <param name="format">The format string for the exception message.</param>
    ///
    /// <param name="args">Formatting arguments.  If no arguments are provided, <pr>format</pr> is
    /// 	used as-is, without formattiong.</param>
    ///
    public ConfigurationException(ConfigurationElement element, int lineOffset, string format, params object[] args) :
        base(String.Format("Line {0}: {1}", element.LineNumber + lineOffset, Util.Format(format, args)))
    {
    }

    /// <summary>
    /// Creates an instance of this exception class.  Formats an exception message, and includes an
    /// inner exception.
    /// </summary>
    ///
    /// <param name="ex">TODO</param>
    ///
    /// <param name="element">The <r>ConfigurationElement</r> that's related to this exception.</param>
    ///
    /// <param name="format">The format string for the exception message.</param>
    ///
    /// <param name="args">Formatting arguments.  If no arguments are provided, <pr>format</pr> is
    /// 	used as-is, without formattiong.</param>
    ///
    public ConfigurationException(Exception ex, ConfigurationElement element, string format, params object[] args) :
        base(String.Format("Line {0}: {1}", element.LineNumber, Util.Format(format, args)), ex)
    {
    }

    /// <summary>
    /// Wraps this <r>ConfigurationException</r>, which includes XML line number information but
    /// doesn't identify the configuration file, in a <r>FatalException</r>, which does.
    /// </summary>
    ///
    /// <param name="fileLabel">The configuration file name or label.</param>
    ///
    public FatalException ToFatalException(string fileLabel)
    {
        return new FatalException(this, "Error configuring HomeUX; see \"{0}\"", fileLabel);
    }

    /// <summary>
    /// If a given exception is a <r>ConfigurationException</r>, which includes XML line number
    /// information but doesn't identify the configuration file, it's wrapped in a
    /// <r>FatalException</r>, which does.  Otherwise, the given exceptin is returned as-is.
    /// </summary>
    ///
    /// <param name="ex">The given exception.</param>
    ///
    /// <param name="fileLabel">The configuration file name or label.</param>
    ///
    public static Exception ToFatalException(Exception ex, string fileLabel)
    {
        var configurationException = ex as ConfigurationException;
        if (configurationException != null)
            return configurationException.ToFatalException(fileLabel);
        else
            return ex;
    }

    public static ConfigurationException UnexpectedElement(ConfigurationElement element)
    {
        return new ConfigurationException(element, "Unexpected element: <{0}>", element.Name);
    }
}

/// <summary>
/// Indicates an error that occurred when using a <r>Driver</r>.
/// </summary>
///
public class DriverException : Exception
{
    /// <summary>
    /// Creates an instance of this exception class.  Accepts an optional inner exception.
    /// </summary>
    ///
    /// <param name="innerException">The inner exception.</param>
    ///
    /// <param name="message">The exception message.</param>
    ///
    public DriverException(Exception innerException, string message) :
        base(message, innerException)
    {
    }

    /// <summary>
    /// Creates an instance of this exception class.  Formats an exception message.
    /// </summary>
    ///
    /// <param name="format">The format string for the exception message.</param>
	///
    /// <param name="args">Formatting arguments.  If no arguments are provided, <pr>format</pr> is
	/// 	used as-is, without formattiong.</param>
    ///
	public DriverException(string format, params object[] args) :
		this(null, Util.Format(format, args))
	{
	}
}

public enum DriverState
{
    Uninitialized,
    FailedInitialization,
    Enabling,
    EnablingThenDisable, // after enabling is complete, immediately call Disable()
    Enabled,
    Disabling,
    DisablingThenEnable, // after disabling is complete, immediately call Enable()
    Disabled
}

}

